/*
 * Copyright (c) 2016. 恒昌互联网运营中心.
 */
package com.credithc.eagleeye.plugins.mvc;
import com.alibaba.fastjson.JSON;
import com.credithc.eagleeye.agent.factory.EagleEyeAgentFactory;
import com.credithc.eagleeye.agent.util.HostUtil;
import com.credithc.eagleeye.constants.EagleEyeConstant;
import com.credithc.eagleeye.constants.RpcTypeEnum;
import com.credithc.eagleeye.exception.EagleEyeIgnoreException;
import com.credithc.eagleeye.plugins.annotation.EagleEyeIgnoreAnnotation;
import com.credithc.eagleeye.plugins.util.ClassUtil;
import com.credithc.eagleeye.plugins.util.IOUtil;
import com.credithc.eagleeye.plugins.util.IpUtil;
import com.credithc.eagleeye.plugins.util.LogUtil;
import com.credithc.eagleeye.thread.EagleEyeThreadContext;
import com.credithc.eagleeye.thread.TraceInfo;
import com.credithc.eagleeye.util.FastJsonConvert;
import com.credithc.eagleeye.util.ThrowableToStringArray;
import com.credithc.eagleeye.vo.RpcTraceInfoVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.FastDateFormat;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.MDC;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.Map;
/**
 * 鹰眼切面
 * 要排除feign上的注解
 * @since 1.0.9
 */
@SuppressWarnings("Duplicates")
@Aspect
@Slf4j
public class EagleEyeMvcAspect {
    private static final FastDateFormat ISO_DATETIME_TIME_ZONE_FORMAT_WITH_MILLIS = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSSZZ");
    private static int MAX_REQUEST_BODY_LENGTH = 4000;

    public EagleEyeMvcAspect() {
        log.info("EagleEyeMvcAspect init..");
    }

    /**
       要排除Feign上的RequestMapping等注解,所以该类上必须有Controller或RestController注解
     */
    @Pointcut("(@annotation(org.springframework.web.bind.annotation.RequestMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.GetMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.PostMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.PutMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.PatchMapping)) " +
            "&& (@within(org.springframework.stereotype.Controller) || @within(org.springframework.web.bind.annotation.RestController)) " +
            "&& !execution(* org.springframework.boot.autoconfigure.web..*.BasicErrorController.*(..))" +
            "")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.debug("EagleEyeMvcAspect around start...");
        Object resultObject = null;
        if (ClassUtil.springWebPresent && RequestContextHolder.getRequestAttributes()!=null && ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest() != null) {
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            Method method = methodSignature.getMethod();
            EagleEyeIgnoreAnnotation ignoreAnnotation = method.getAnnotation(EagleEyeIgnoreAnnotation.class);
            if (ignoreAnnotation != null) {
                return joinPoint.proceed();
            }
            String methodName = method.getName();
            Object target = joinPoint.getTarget();
            String serviceName = target.getClass().getSimpleName();
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

            EagleEyeThreadContext.init();
            TraceInfo traceInfo = new TraceInfo();
            String hcTraceId = request.getHeader(EagleEyeConstant.HC_TRACE_ID);
            if (StringUtils.isNotBlank(hcTraceId)) {
                traceInfo.setTraceId(hcTraceId);
            } else {
                //为了兼容credithc-framework-log模块中的trace
                String mdcTraceId = MDC.get(EagleEyeConstant.TRACE_ID);
                if (StringUtils.isNotBlank(mdcTraceId)) {
                    traceInfo.setTraceId(mdcTraceId);
                }
            }
            String hcRpcId = request.getHeader(EagleEyeConstant.HC_RPC_ID);
            if (hcRpcId != null && hcRpcId.length() > 0) {
                traceInfo.setRpcId(hcRpcId);
                traceInfo.addHierarchy();
                traceInfo.addSequenceNo();
            }


            RpcTraceInfoVo rpcTraceInfoVo = new RpcTraceInfoVo();
            rpcTraceInfoVo.setRequestDateTime(ISO_DATETIME_TIME_ZONE_FORMAT_WITH_MILLIS.format(new Date()));
            rpcTraceInfoVo.setTraceId(traceInfo.getTraceId());
            rpcTraceInfoVo.setRpcId(traceInfo.getRpcId());
            rpcTraceInfoVo.setRpcType(RpcTypeEnum.HTTP.getRpcName());
            rpcTraceInfoVo.setServiceCategory("mvc");
            rpcTraceInfoVo.setServiceName(serviceName);
            rpcTraceInfoVo.setMethodName(methodName);

            if (EagleEyeAgentFactory.getInstance().getEagleEyeAgentConfig().isEnableMvcParameter()) {
                Map<String, String[]> parameterMap = request.getParameterMap();
                StringBuilder requestBodySb = new StringBuilder();
                if ((parameterMap != null && parameterMap.size() > 0)) {
                    requestBodySb.append("parameterMap:");
                    requestBodySb.append(FastJsonConvert.convertObjectToJSON(parameterMap));
                    requestBodySb.append("\n");
                }
                //读取body，但是必须在过滤器里替换
                try {
                    if (request instanceof ContentCachingRequestWrapper) {
                        String body = IOUtil.toString(request.getInputStream(), "UTF-8");
                        if (body != null && body.length() > 0) {
                            requestBodySb.append("body:");
                            requestBodySb.append(body);
                            rpcTraceInfoVo.setRequestSize(requestBodySb.length());
                        }
                    }
                } catch (IOException e) {
                }
                if (requestBodySb.length() <= MAX_REQUEST_BODY_LENGTH) {
                    rpcTraceInfoVo.setRequestJson(requestBodySb.toString());
                } else {
                    rpcTraceInfoVo.setRequestJson(requestBodySb.substring(0, MAX_REQUEST_BODY_LENGTH) + "..");
                }
            }
            rpcTraceInfoVo.setServerHost(HostUtil.getIp() + ":" + request.getServletPath());
            rpcTraceInfoVo.setClientHost(IpUtil.getClientIp(request));
            traceInfo.addHierarchy();
            EagleEyeThreadContext.putTraceInfo(traceInfo);
            MDC.put("traceId", traceInfo.getTraceId());
            MDC.put("rpcId", traceInfo.getRpcId());
            MDC.put("clientIp", IpUtil.getClientIp(request));

            long beginTime = System.currentTimeMillis();
            //执行调用
            try {
                resultObject = joinPoint.proceed();
                rpcTraceInfoVo.setRunTime(System.currentTimeMillis()-beginTime);
                rpcTraceInfoVo.setResult("OK");
                if (EagleEyeAgentFactory.getInstance().getEagleEyeAgentConfig().isEnableMvcParameter()) {
                    rpcTraceInfoVo.setResponseJson(transform2JsonStr(resultObject));
                }
            } catch (Exception ex) {
                if (!(ex instanceof EagleEyeIgnoreException)) {
                    //ERROR
                    rpcTraceInfoVo.setResult("ERROR");
                    rpcTraceInfoVo.setRunTime(System.currentTimeMillis()-beginTime);
                    //异常的结果
                    String[] traceStr = ThrowableToStringArray.convert(ex);
                    StringBuilder builder = new StringBuilder();
                    for(String trace : traceStr) {
                        builder.append(trace);
                        builder.append("\n");
                    }
                    rpcTraceInfoVo.setResponseJson(builder.toString());
                } else {
                    //OK
                    rpcTraceInfoVo.setResult("OK");
                }
                throw ex;
            } finally {
                EagleEyeThreadContext.removeTraceInfo();
                MDC.remove("traceId");
                MDC.remove("rpcId");
                MDC.remove("clientIp");
                EagleEyeAgentFactory.getInstance().send2EagleEye(rpcTraceInfoVo);
                LogUtil.printSplunkLog(rpcTraceInfoVo);
            }
        } else {
            //执行调用
            resultObject = joinPoint.proceed();
        }
        log.debug("EagleEyeMvcAspect around stop...");
        return resultObject;
    }

    private String transform2JsonStr(Object argument) {
        if (ClassUtil.servletPresent && (argument instanceof ServletRequest || argument instanceof ServletResponse)) {
            return "...";
        }
        if (ClassUtil.springWebPresent && (argument instanceof MultipartFile)) {
            return "...";
        }
        try {
            return JSON.toJSONString(argument);
        } catch (Exception ex) {
        }
        return "";
    }
}
